package com.loong.android.socket;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * Socket客户
 * 需要权限：android.permission.INTERNET
 */
public class SocketClient {
    private int bufferLen = 4096;
    private boolean autoDealBuffer = true;
    private SocketEvent socketEvent;
    private ClientSocketThread clientSocketThread;

    /**
     * 置缓存区尺寸，默认为4096
     *
     * @param bufferLen 字节数
     */
    public void setBufferLen(int bufferLen) {
        this.bufferLen = bufferLen;
    }

    /**
     * 设置接收数据时，是否自动处理。若设置为false，则发送数据需要使用send2函数，发送不经过任何加工的原始数据。
     * 若设置为true，则发送数据需要使用send函数，发送数据时，在数据前带上4字节的数据表示数据长度。在收到数据时，
     * 先读取数据长度值，再根据数据长度读取数据，这样可以解决数据传输过程的半包、粘包等问题。
     *
     * @param autoDealBuffer 是否启用
     */
    public void setAutoDealBuffer(boolean autoDealBuffer) {
        this.autoDealBuffer = autoDealBuffer;
    }

    /**
     * 置套接字事件
     *
     * @param socketEvent 事件回调
     */
    public void setSocketEvent(SocketEvent socketEvent) {
        this.socketEvent = socketEvent;
    }

    /**
     * 连接服务器
     *
     * @param ip ip地址
     * @param port 端口号，范围：0~65535
     * @param timeout 超时，单位为ms，默认为5000，即5s
     * @param event 连接事件回调
     */
    public void connect(String ip, int port, int timeout, ConnectEvent event) {
        new Thread(() -> {
            try {
                Socket socket = new Socket(ip, port);
                socket.setSoTimeout(timeout);
                //连接成功后，开始接收和处理数据
                clientSocketThread = new ClientSocketThread(socket, ip, port);
                if(event != null) event.onComplete(true, ip, port);
            }catch (Exception e) {
                e.printStackTrace();
                if(event != null) event.onComplete(false, ip, port);
            }
        }).start();
    }

    /**
     * 连接服务器
     *
     * @param ip ip地址
     * @param port 端口号，范围：0~65535
     * @param timeout 超时，单位为ms，默认为5000，即5s
     */
    public void connect(String ip, int port, int timeout) {
        connect(ip, port, timeout, null);
    }

    /**
     * 连接服务器
     *
     * @param ip ip地址
     * @param port 端口号，范围：0~65535
     * @param event 连接事件回调
     */
    public void connect(String ip, int port, ConnectEvent event) {
        connect(ip, port, 5000, event);
    }

    /**
     * 断开与服务端的连接
     */
    public void disconnect() {
        if(clientSocketThread == null) return;
        clientSocketThread.close();
        clientSocketThread = null;
    }

    /**
     * 连接服务器
     *
     * @param ip ip地址
     * @param port 端口号，范围：0~65535
     */
    public void connect(String ip, int port) {
        connect(ip, port, 5000, null);
    }

    /**
     * 发送数据，自动在数据前加上数据长度。
     * 在收到数据时，先读取数据长度值，再根据数据长度读取数据，这样可以解决数据传输过程的半包、粘包等问题。
     * @param data 待发送的数据
     * @param event 发送完毕的事件回调
     */
    public void send(byte[] data, SendEvent event) {
        if(clientSocketThread == null) {
            if(event != null) event.onComplete(false);
            return;
        }
        clientSocketThread.send(data, event);
    }

    /**
     * 发送数据，自动在数据前加上数据长度。
     * 在收到数据时，先读取数据长度值，再根据数据长度读取数据，这样可以解决数据传输过程的半包、粘包等问题。
     * @param data 待发送的数据
     */
    public void send(byte[] data) {
        if(clientSocketThread == null) return;
        clientSocketThread.send(data, null);
    }

    /**
     * 发送原始数据，需要调用setAutoDealBuffer(false)，关闭数据自动处理，否则会导致接收数据错乱。
     *
     * @param data 待发送的数据
     * @param event 发送完毕的事件回调
     */
    public void send2(byte[] data, SendEvent event) {
        if(clientSocketThread == null) {
            if(event != null) event.onComplete(false);
            return;
        }
        clientSocketThread.send2(data, event);
    }

    /**
     * 发送原始数据，需要调用setAutoDealBuffer(false)，关闭数据自动处理，否则会导致接收数据错乱。
     *
     * @param data 待发送的数据
     */
    public void send2(byte[] data) {
        if(clientSocketThread == null) return;
        clientSocketThread.send2(data, null);
    }

    /**
     * 判断客户端是否成功绑定到地址
     */
    public boolean isBound() {
        return clientSocketThread != null && clientSocketThread.getSocket().isBound();
    }

    /**
     * 判断客户端是否已连接
     */
    public boolean isConnected() {
        return clientSocketThread != null && clientSocketThread.getSocket().isConnected();
    }

    /**
     * 判断客户端是否已关闭
     */
    public boolean isClosed() {
        return clientSocketThread == null || clientSocketThread.getSocket().isClosed();
    }

    /**
     * 判断客户端的输入流是否关闭
     */
    public boolean isInputShutdown() {
        return clientSocketThread == null || clientSocketThread.getSocket().isInputShutdown();
    }

    /**
     * 判断客户端的输出流是否关闭
     */
    public boolean isOutputShutdown() {
        return clientSocketThread == null || clientSocketThread.getSocket().isOutputShutdown();
    }

    /**
     * 关闭输出流
     */
    public void shutdownOutput() {
        if(clientSocketThread == null) return;
        try {
            clientSocketThread.getSocket().shutdownOutput();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 关闭输入流
     */
    public void shutdownInput() {
        if(clientSocketThread == null) return;
        try {
            clientSocketThread.getSocket().shutdownInput();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 取端口号
     */
    public int getPort() {
        if(clientSocketThread == null) return 0;
        return clientSocketThread.getSocketPort();
    }

    /**
     * 取IP地址
     */
    public String getIp() {
        if(clientSocketThread == null) return null;
        return clientSocketThread.getSocketIp();
    }

    class ClientSocketThread extends Thread {
        Socket socket;
        private InputStream is;
        private OutputStream os;
        private DataInputStream dis;
        private boolean isRun = true;
        private final String ip;
        private final int port;
        //需要接收数据的长度
        private int needReceiveLen = 0;
        //处理步骤，0=读数据长度；1=读数据
        private int dealStep = 0;
        private byte[] dataBuffer;
        private final byte[] lenBuffer = new byte[4];
        private int dbIndex = 0;
        private int lenIndex = 0;

        public ClientSocketThread(Socket socket, String ip, int port) {
            this.socket = socket;
            this.ip = ip;
            this.port = port;
            try {
                os = this.socket.getOutputStream();
                is = this.socket.getInputStream();
                dis = new DataInputStream(is);
                start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public Socket getSocket() {
            return this.socket;
        }

        public String getSocketIp() {
            String tmp = this.socket.getInetAddress().toString();
            return tmp.startsWith("/") ? tmp.substring(1) : tmp;
        }

        public int getSocketPort() {
            return this.socket.getPort();
        }

        public void send2(final byte[] data, SendEvent event) {
            if (os == null || data == null) {
                if(event != null) event.onComplete(false);
                return;
            }
            new Thread(() -> {
                try {
                    os.write(data);
                    os.flush(); //强制送出数据
                    if(event != null) event.onComplete(true);
                } catch (IOException e) {
                    e.printStackTrace();
                    if(event != null) event.onComplete(false);
                }
            }).start();
        }

        /**
         * 在数据前加4字节数据表示数据长度，接收数据时通过数据长度拼接数据，解决数据传输过程的半包、粘包的问题
         *
         * @param data 待发送的数据
         */
        public void send(final byte[] data, SendEvent event) {
            if (os == null || data == null) {
                if(event != null) event.onComplete(false);
                return;
            }
            new Thread(() -> {
                try {
                    int sendLen = data.length;
                    //给数据添加包头
                    byte[] senBuffer = new byte[4 + sendLen];
                    System.arraycopy(DataUtil.intToBytes(sendLen), 0, senBuffer, 0, 4);
                    System.arraycopy(data, 0, senBuffer, 4, sendLen);
                    os.write(senBuffer);
                    os.flush(); //强制送出数据
                    if(event != null) event.onComplete(true);
                } catch (IOException e) {
                    e.printStackTrace();
                    if(event != null) event.onComplete(false);
                }
            }).start();
        }

        public void close() {
            isRun = false;
            try {
                if (is != null) is.close();
                if (os != null) os.close();
                if (dis != null) dis.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        private void dealBuffer(byte[] data, int dataLen) {
            //如果不需要自动处理分包，则直接返回。
            if (!autoDealBuffer) {
                byte[] output = new byte[dataLen];
                System.arraycopy(data, 0, output, 0, dataLen);
                if (socketEvent != null) socketEvent.onReceive(ip, port, output);
                return;
            }
            //处理的数据位置
            int dealIndex = 0;
            while (dealIndex < dataLen) {
                if(dealStep == 0) {
                    int tmpLen = Math.min(4 - lenIndex, dataLen - dealIndex);
                    System.arraycopy(data, dealIndex, lenBuffer, lenIndex, tmpLen);
                    lenIndex += tmpLen;
                    dealIndex += tmpLen;
                    if(lenIndex == 4) {
                        needReceiveLen = DataUtil.bytesToInt(lenBuffer);
                        if (needReceiveLen >= 0) dataBuffer = new byte[needReceiveLen];
                        dbIndex = 0;
                        lenIndex = 0;
                        dealStep = 1;
                    }
                }else {
                    int tmpLen = Math.min(needReceiveLen - dbIndex, dataLen - dealIndex);
                    System.arraycopy(data, dealIndex, dataBuffer, dbIndex, tmpLen);
                    dbIndex += tmpLen;
                    dealIndex += tmpLen;
                    if (dbIndex >= needReceiveLen) {
                        dealStep = 0;
                        if (socketEvent != null) socketEvent.onReceive(ip, port, dataBuffer);
                    }
                }
            }
        }

        @Override
        public void run() {
            super.run();
            byte[] buffer = new byte[bufferLen];
            int rcvLen;
            while (isRun && !socket.isClosed() && !socket.isInputShutdown()) {
                try {
                    if ((rcvLen = dis.read(buffer)) != -1) {
                        dealBuffer(buffer, rcvLen);
                    } else {
                        isRun = false;
                        if (socketEvent != null) socketEvent.onDisconnect(ip, port);
                        break;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            close();
        }
    }

    /**
     * 发送数据的接口事件
     */
    public abstract static class SendEvent {
        /**
         * 数据发送完毕事件。注意，本事件在多线程中触发，不能直接操作UI。
         */
        public void onComplete(boolean res) {

        }
    }

    /**
     * 连接的接口事件
     */
    public abstract static class ConnectEvent {
        /**
         * 连接完毕。注意，本事件在多线程中触发，不能直接操作UI。
         *
         * @param res 结构
         * @param ip ip地址
         * @param port 端口号
         */
        public void onComplete(boolean res, String ip, int port) {

        }
    }

    /**
     * 套接字的接口事件
     */
    public abstract static class SocketEvent {
        /**
         * 收到来自服务端的数据。注意，本事件在多线程中触发，不能直接操作UI。
         *
         * @param ip 服务端ip地址
         * @param port 端口号
         * @param data 数据
         */
        public void onReceive(String ip, int port, byte[] data) {

        }

        /**
         * 与服务器的连接被断开。注意，本事件在多线程中触发，不能直接操作UI。
         */
        public void onDisconnect(String ip, int port) {

        }
    }
}
